Skip to content

3. AI Agent 与工具调用特训指南

本指南结合您简历中 "Spring AI Advisor 链式 Agent 调度管道""12 种意图分类动态工具白名单""用户画像与记忆注入机制""SafeToolCallAdvisor 动态签名死循环熔断" 场景进行深度定制,全面采用 Why - What - How - Deep 极简源码/原理速成结构。


一、 AI Agent 核心概念与编排架构

Why(传统 Workflow 与 Chatbot 的局限性)

  • Chatbot 局限性:基于单轮或简单多轮问答,无状态,无法主动调用外部系统获取信息或触发操作,缺乏“手”的能力。
  • Workflow (工作流) 局限性:流程在开发期通过 DAG (有向无环图) 严格定死。一旦真实业务场景偏离了预设分支(如用户说话兜圈子、输入格式异常),Workflow 会直接卡死,缺乏对不确定性的灵活性。
  • Agent 的优越性:大模型拥有了“规划能力”与“工具箱”。系统仅给定最终 Goal,Agent 能自主在 Loop 循环中“观察状态 -> 规划路径 -> 采取行动 -> 反思结果”,动态处理极其复杂的长链路非确定性任务。

What(Agent 标准定义与三方模式)

  • Agent 的标准定义:能够自主感知环境、进行推理规划、选择并调用工具、在闭环 Loop 中持续迭代直到目标完成的智能体。
  • 核心三方对比
    维度ChatbotWorkflowAgent
    决策大脑规则匹配 / 单轮生成DAG 预定义流程LLM 动态自主规划
    路径控制无流程 / 固定跳转开发者硬编码确定Agent 自主探索路径
    工具使用固定节点调用固定接口动态选择工具与入参
    循环结构条件分支,不支持反思环感知-规划-行动-反思闭环 Loop
  • 编排三模式
    • Workflow:线性/条件分支 DAG。适合稳定、确定性要求 100% 的结算等业务。
    • Graph:允许循环和状态回溯的复杂图结构。
    • Loop:自回归式循环规划。通过 LLM 的 ReAct (Reasoning + Acting) 驱动,不断逼近最终目标。

How(sky-ai 责任链 Agent 闭环)

  • sky-ai 基于 Spring AI Advisor 责任链 组装起了一个高性能的 Agent Loop。
  • 处理流程七阶段时序流
    1. 意图二度识别IntentRecognitionAdvisor 优先拦截,优先匹配 preRecognizedIntent 预识别旁路以跳过动态 LLM 调用。若是新会话,则异步提取长期记忆的 Profile Summary(画像摘要) 拼接注入最头部,结合会话历史产出高精度意图。
    2. FAQ 缓存短路FaqSemanticCacheAdvisor 拦截,若意图为 FAQ,立刻将提问向量化,在本地缓存库进行相似度 HNSW 搜索。若相似度突破阈值,直接短路答复,Token 零消耗、5ms 极致响应。
    3. 自适应画像注入UserContextAdvisor 解析意图,动态匹配注入级别 (NONE/SUMMARY/FULL) 并装配相关的 allowedTools 动态白名单。
    4. 会话历史加载:内置的 MessageChatMemoryAdvisor 基于 RedisChatMemoryRepository 自动加载并注入该用户最近的滑动窗口会话。
    5. 条件式 RAG 挂载RagAdvisor 条件运行。只有在 shouldUseRag 成立时(isKnowledge() [如 FAQ] 或涉及退款等高风险纠纷意图),才触发向量召回并 prepend 系统指令。
    6. 工具动态过滤ToolFilterAdvisor 在大模型调用前拦截,从注册中心中基于前面算出的 allowedTools 固定并覆盖 ChatOptions 中的工具集,形成物理级别的工具沙箱。
    7. 签名审计与防死循环:最后一关 SafeToolCallAdvisor 拦截,跟踪被调用工具并校验哈希签名。若监测到参数签名重复自旋或者工具交互突破 4 轮上限,瞬间强行熔断并输出优雅的保底兜底文案。
  • 对线重点👉 跳至:sky-ai Advisor 链调度管道大厂对线

Deep(自回归推理漂移与不确定性控制)

  • Agent 推理累积漂移:在长链 Loop 中,模型每一次的 Token 采样误差和工具调用的微小偏离,都会作为下一轮 Loop 的输入上下文。随着 Loop 轮数增加,误差呈指数级累积,导致 Agent 的目标彻底偏离(Goal Drifting)。
  • 工程防范手段:必须设定硬性的 Loop 最大终止阀值(例如限制 MAX_TOOL_CALL_ROUNDS = 4),并在 Prompt 中使用强状态机制(如 State Machine Prompting),约束模型在每一步必须输出当前所处的具体状态,用确定性的规则截断不确定性的自回归发散。

二、 记忆系统(Agent Memory)与上下文工程

Why(大模型无状态性与上下文窗口暴涨的冲突)

  • 无状态硬伤:大模型 API 本质是无状态的(Stateless),每次 HTTP 请求之间毫无关联。为了维持对话连贯性,必须将历史聊天记录和用户偏好作为上下文(Context)反复传给大模型。
  • 上下文爆满问题:如果毫无节制地将用户所有的历史信息、订单记录、偏好设置全部塞入 Prompt,不仅会带来高昂的 Token 费用和高昂的延迟,还会触发 Lost in the Middle 效应,导致模型遗忘核心指令。
  • 精细化管理的必要性:必须构建分层的记忆系统,根据意图动态、按需、按粒度注入上下文。

What(长期/短期记忆与上下文工程)

  • 短期记忆 (Session Memory):记录当轮对话的消息历史(Chat History),用于维持当前话题的连贯性。通常保存在 Redis 或内存中,设定滑动窗口大小。
  • 长期记忆 (Persistent Memory):提取自历史多轮对话或用户显式设置的结构化特征事实(Facts),如饮食禁忌、口味偏好。持久化于关系型数据库(如 UserMemoryFact 实体)。
  • Prompt Eng vs Context Eng 的本质区别
    • Prompt Engineering:关注 Prompt 指令本身的措辞、结构(如角色扮演、One-Shot 示例),属于“静态模板设计”。
    • Context Engineering:关注“在什么时间节点、根据什么业务指标、以什么优先级和排版顺序,将动态生成的业务数据(检索到的知识、提取到的偏好记忆、多轮会话)拼装进 LLM 的上下文窗口中”,属于“动态信息流编排”。

How(sky-ai 分级记忆注入与意图联动)

  • sky-ai 通过 UserContextAdvisor 实现了意图驱动的分级记忆注入机制
    SHOP_STATUS           → ProfileInjectionLevel.NONE    → 简单查询,不注入记忆,Token 损耗降为 0
    ORDER_STATUS/CANCEL   → ProfileInjectionLevel.SUMMARY → 仅注入画像摘要 (如: "VIP 铂金用户")
    MENU_QUERY/CART       → ProfileInjectionLevel.FULL    → 注入完整画像 + 口味偏好 + 饮食禁忌 (如: "不吃辣")
  • 在调用意图识别 LLM 前,通过 IntentRecognitionAdvisor 将提取的用户画像摘要拼接到 Prompt 顶部,显著提升意图匹配的准确度。
  • 对线重点👉 跳至:sky-ai 用户记忆系统大厂对线

Deep(高并发下 WebFlux 上下文不可变突变线上踩坑)

  • 高并发线程安全冲突:在 Web Flux 响应式微服务环境下,大模型请求上下文 ChatClientRequest.context() 底层往往是由 immutable map(如 Collections.unmodifiableMap)进行包装保护的。
  • 踩坑现场:早期版本中,拦截器(Advisor)内部直接通过 chatClientRequest.context().put("allowedTools", Set.of(...)) 进行就地物理修改,在高并发请求并发打入时,直接触发 UnsupportedOperationException 异常,导致整个 AI 微服务瘫痪。
  • Builder 不可变重构方案:在 Advisor 拦截中,必须采用只读防御性拷贝,利用 chatClientRequest.mutate().context(copiedMap).build() 重新实例化不可变的请求对象。这不仅彻底解决了线程安全并发异常,更遵循了函数式编程的“副作用最小化”原则。

三、 工具调用安全与熔断机制

Why(自主 Agent 调用外部 API 的安全死穴)

  • 参数幻觉瞎猜:大模型极易根据直觉胡乱猜测 API 入参(例如调用删除订单接口 deleteOrder(orderId) 时,模型幻觉编造了一个不属于当前用户的 orderId = 999999)。若不设防,将造成致命的水平越权漏洞
  • 无限循环耗尽 Token:当模型调用工具返回报错(如“格式不合法”)时,大模型会基于报错信息,进行自我反思并尝试再次调用该工具。如果错误没有被根治,模型会像脱缰的野马一样,在一次请求内反复发起几十次 API 调用,直至单次会话 Token 彻底耗尽、产生巨额账单并锁死线程。

What(三维防线体系)

  1. 签名防重检测:对单次循环中产生的工具名和参数字符串进行哈希签名校验。
  2. 轮次上限熔断:强制限制单次请求中工具调用的交互往返次数。
  3. 参数沙箱拦截:通过强绑定鉴权上下文,对大模型吐出来的参数进行“反向缓存匹配验证”。

How(sky-ai 熔断器与水平越权防护)

  • SafeToolCallAdvisor 中实现签名重复判定与 MAX_TOOL_CALL_ROUNDS = 4 的强限流熔断,熔断后通过 stop() 替换原始大模型响应为优雅的业务降级话术。
  • OrderTools 业务执行前,基于当前鉴权的 ToolContext 获取当前用户 ID,并比对最近订单缓存哈希。非法或越权的订单 ID 强行截断,杜绝参数穿透。
  • 对线重点👉 跳至:SafeToolCallAdvisor 熔断机制大厂对线

Deep(水平越权与参数穿透防御物理隔离底座)

  • 模型越权成因:由于 Function Calling 中的参数完全由 LLM 的推理结果(JSON)反序列化得到,在没有加防的情况下,恶意用户可以通过 Prompt 注入(如“请帮我取消订单,订单号为:别的用户的订单 ID”)欺骗 LLM 提取出非授权参数并执行。
  • 物理隔离实现机理
    • 在 Spring AI 入口处,通过 .toolContext(Map.of("userId", currentUserId)) 将安全框架(Spring Security/JWT)鉴权通过的真实用户 ID 灌入底层 ToolContext。大模型无法接触和篡改这个 ToolContext
    • 在具象化执行的方法签名中声明 ToolContext context
    • 在方法体内,利用 ToolUser.userId(context) 强制取出真实用户 ID,限制 SQL 查询条件必须带上 where user_id = current_user_id
    • 哈希混淆过滤:在 resolveOrderId 拦截阶段,前端展现的订单号为混淆后的 Hash ID,客户端在 Redis 缓存中为当前用户维护一张最近 10 次订单的映射表。大模型给出的参数必须在这个映射表内,否则强行拒绝执行并回显错误。这样即便模型通过参数爆破乱猜,也绝不可能触碰到任何越权数据。

四、 开放协议与工程骨架(MCP & Harness)

Why(微服务工具爆炸与基础设施碎片化)

  • 开发维护灾难:在大型企业级 Agent 场景下,工具库(包含查库存、发邮件、报表生成等数百个微服务)规模庞大。若将这些工具逻辑全部以硬编码(Java Method Annotation)形式塞进主 AI 应用的包里,会导致应用臃肿不堪,每次工具微调都需要重新编译打包整个 AI 网关。
  • 模型接入碎片化:不同厂牌大模型对于 Function Calling 的定义格式细微不一致,导致工程适配极为繁琐。

What(MCP 协议与 Harness 骨架)

  • MCP (Model Context Protocol, 模型上下文协议)
    • 由 Anthropic 牵头制定的开源标准协议。
    • 核心理念:将大模型应用(MCP Client)与具体的工具源(MCP Server)进行松耦合物理隔离。MCP Server 独立部署,通过标准化的 JSON-RPC 2.0 协议向 Client 暴露其可用的 Resource(数据源)、Prompt(模板)和 Tool(执行逻辑)。
  • MCP vs Function Calling vs Tool Calling 区别
    维度Function CallingTool CallingMCP
    物理位置客户端硬编码客户端硬编码独立部署的外部 MCP Server
    网络协议模型特定 JSON模型特定 JSON标准 JSON-RPC 2.0 (HTTPS / SSE / StdIO)
    扩容升级需要发布主服务需要发布主服务MCP Server 独立迭代热插拔
  • Harness Engineering (骨架工程)
    • 定义:包裹在大模型核心之上的工程化支撑网(即由拦截器、重试模板、降级组件、安全隔离、用户记忆等构成的体系)。
    • 名言:一个工业级 AI Agent 系统的质量,30% 取决于大模型基座,70% 取决于 Harness 工程的设计深度。
  • Agent Skills(技能包):将特定的 Prompt 模板、专有 Tool 定义以及执行后的过滤规则,整体打包成独立的、可热插拔的 Skill 目录,实现 Agent 能力的乐高式积木化组装。

How(生产级 MCP 鉴权与审计流设计)

  • 在大型分布式 Agent 体系中,主 Client 通过 SSE 协议与多台专有的 MCP Server 建立长连接,并通过网关在 JSON-RPC 请求头部附带 Bearer Token。
  • 审计链设计:在 MCP Client 的网关层拦截所有 JSON-RPC 帧,将 method: "tools/call" 请求中的 arguments 连同当前操作员身份同步打入 ELK 审计日志中,实现高风险操作的可溯源性。

Deep(MCP 底层 JSON-RPC 协议帧与安全沙箱隔离)

  • MCP JSON-RPC 2.0 通信帧解析
    json
    // MCP Client 发起工具调用
    {
      "jsonrpc": "2.0",
      "method": "tools/call",
      "params": {
        "name": "calculate_tax",
        "arguments": {
          "income": 10000
        }
      },
      "id": "req_001"
    }
  • 物理安全沙箱(Deno/Wasm 隔离):为了防止恶意的 MCP Server 被攻击后回传含有系统命令的恶意脚本(Command Injection),在生产级 MCP 架构中,MCP Client 接收到 Server 回传的执行结果后,需将其放进无物理网络权限的 Wasm 沙箱环境 或严格限制系统调用的容器内进行前置反序列化,防范任意代码执行(RCE)漏洞。

五、 简历亮点与大厂面试对线(袁志刚专属)

1. Agent Loop 与【sky-ai Advisor 链调度管道】

  • 面试官切入点

    "你实现的 AI 客服 Agent 是一个典型的 Agent Loop 吗?请讲一下你的 Agent 一次完整请求的循环流程,以及你是如何控制循环终止的。"

  • 袁志刚专属特训回答模版
    1. 基于 Spring AI Advisor 责任链的闭环 Loop:我们在 sky-ai 中摒弃了传统的硬编码 Workflow 或复杂的 DAG 框架,深度基于 Spring AI Advisor 责任链构建了一个高灵活性、高性能的 Agent Loop。
    2. 完整执行四阶段时序流程
      • 观察感知:请求进来,IntentRecognitionAdvisor(优先级最高)调用轻量级分类模型,分析当前用户输入,并结合 Redis 历史会话构建包含当前意图、实体的 IntentRecognitionResult 并塞入上下文 ChatClientRequest.context()
      • 动态规划:紧接着 UserContextAdvisor 拦截请求,解析该意图。针对特定意图拉取最契合的用户画像及饮食禁忌等长期记忆,同时动态生成该意图专属的动态工具白名单,阻止无关工具的 Prompt 污染。
      • 行动阶段ToolFilterAdvisorDynamicToolCallbackRegistry 动态工具注册中心中,基于前面得出的工具白名单筛选出 ToolCallback 列表并织入 Prompt Options。大模型自主进行规划并输出 tool_calls 执行业务工具。
      • 反思与安全熔断SafeToolCallAdvisor(优先级最低)作为最后一层防线。在每一轮工具调用返回后,计算签名哈希检测防重,并校验循环次数。若安全无误则继续循环;若触发死循环或超时,立即拦截阻断,输出优雅的兜底文案。
    3. 核心控制终止:我们通过 1) 签名重复去重、2) 限制最大工具往返轮次为 4 轮、3) 模型生成正常终结标记(Assistant 回复不带工具调用) 这三大条件联合控制 Loop 的退出,在生产环境中彻底降服了自回归生成失控的问题。

🖥️ 核心支撑源码:Agent 意图识别与入口编排

java
// AgentChatService.java - 责任链及入口管道编排
public String ask(String question, String conversationId, String userId, IntentRecognitionResult preIntent) {
    // 1. 调用 executeCall 触发 ChatClient.Builder 并挂载责任链
    ChatClientResponse response = executeCall(question, conversationId, userId, preIntent, Map.of());
    return extractAnswer(response);
}

private ChatClientResponse executeCall(String question, String conversationId, String userId, 
                                       IntentRecognitionResult preIntent, Map<String, Object> extraContext) {
    Map<String, Object> contextParams = new java.util.LinkedHashMap<>();
    contextParams.put(ChatMemory.CONVERSATION_ID, conversationId);
    contextParams.put("userId", userId);
    
    return chatClientBuilder.build().prompt()
            .advisors(advisors(preIntent).toArray(CallAdvisor[]::new)) // 装载拦截器责任链
            .advisors(advisor -> contextParams.forEach(advisor::param))
            .toolContext(Map.of("userId", userId)) // 将用户ID作为上下文传给 Function 工具做越权防御
            .user(question)
            .call()
            .chatClientResponse();
}

2. Agent Memory 与【sky-ai 用户记忆系统】

  • 面试官切入点

    "你的 Agent 有一个完整的用户记忆系统,包括偏好菜品、口味偏好、饮食禁忌等。请问你的短期记忆和长期记忆是如何设计的?不同的意图场景下记忆注入粒度是如何控制的?"

  • 袁志刚专属特训回答模版
    1. 短期记忆 (Session Memory) 的设计:短期记忆关注会话内部的上下文连贯。我们基于 Spring AI 的 ChatMemory 接口,以 conversationId 为唯一 Key 将消息列表存储在 Redis 缓存中,通过滑动窗口(Sliding Window)保持最近 10 轮对话的上下文。
    2. 长期记忆 (Persistent Memory) 的设计与混合事实更新算法(大厂级核心亮点): 长期记忆负责沉淀跨会话的深度用户事实。我们设计了 UserMemory JPA 实体(对应 user_memory_facts 数据库表),字段严密精简为:userId (String), dietaryPrefs (String), defaultAddress (String), knownIssues (String,长文本运营备注,长度限制 500 字符,在业务上映射为 OPERATIONAL_NOTES)。 为了确保存储极度可信且性能无感,我们部署了 LLM 异步事实提取本地代码强一致性自动持久化混合算法。通过标注了 @Async 异步执行的 MemoryWriterService 在会话结束时异步处理:
      • LLM 异步事实提取与自适应修正/物理删除机制:若会话无待确认卡点且置信度高,我们将 USER 副本传给 LLM 提取 JSON。系统在调用 upsertFact 时支持 修正机制 (corrections) 自动覆盖前后不一致事实。若提取的字段值为 null,表明用户要求物理遗忘(如“我再也不吃辣了”),则底层物理触发 deleteFact 将其从 PostgreSQL 中物理删除
      • 非 LLM 依赖的本地工具强一致性持久化流 (Tool Outcomes Auto-Persistence):由于依靠大模型提取核心事实(订单取消、退款等)响应慢且有安全幻觉,系统率先扫描消息链中的 ToolResponseMessage(保证返回不包含 "FAIL:" 头)。只要特定高风险工具调用成功,系统直接以 TOOL 来源强一致性地向 PostgreSQL 写入/覆写准确事实:
        • ADDRESS_MANAGEMENT ─► 自动解析响应的 JSON 地址实体,若 detail 字段不空,以 TOOL 来源 upsert DEFAULT_ADDRESS 事实。
        • CHANGE_ADDRESS ─► 截取并解析返回的新地址,直接覆盖写入 DEFAULT_ADDRESS
        • CANCEL_ORDER / REQUEST_REFUND ─► 解析成功的响应并提取 orderId 和退款原因,以历史陈述句风格(如:"已取消订单 123(当前系统时间)""已为订单 123 退款:无故未送到")追加写入 OPERATIONAL_NOTES,实现 100% 精准的运营备注事实沉淀。
    3. 意图驱动的分级记忆注入(极致的 Context 净化):为防止长文本污染与无畏的 Token 浪费,我们在 UserContextAdvisor 拦截器中根据 IntentRecognitionResult 实施分级注入策略:
      • 若为 SHOP_STATUS(查询商铺营业状态等),设置注入级别为 NONE,完全不注入任何用户长期画像。
      • 若为 ORDER_STATUS, TRACK_DELIVERY, CANCEL_ORDER, REQUEST_REFUND, CHANGE_ADDRESS, ADDRESS_MANAGEMENT, REPORT_MISSING_ITEM, REORDER,级别设为 SUMMARY,仅注入最新的画像简短摘要。
      • 若为 MENU_QUERY, CART_MANAGEMENT, FAQ, ESCALATE_TO_HUMAN, OTHER,级别设为 FULL,将长期数据库中的详细偏好事实(饮食偏好等)拼装成 System Prompt 模块,注入在 Prompt 的第 0 位(最顶部位置),利用首尾注意力增强效应确保大模型绝不违背用户的饮食偏好。

🖥️ 核心支撑源码:意图驱动工具过滤与请求不可变重构

java
// UserContextAdvisor.java - 根据意图动态过滤工具并注入记忆上下文
@Override
public ChatClientResponse adviseCall(ChatClientRequest chatClientRequest, CallAdvisorChain callAdvisorChain) {
    IntentRecognitionResult intentResult = resolveIntent(chatClientRequest);
    
    // 1. 防御性拷贝只读上下文,注入允许的工具白名单
    Map<String, Object> context = new java.util.HashMap<>(chatClientRequest.context());
    context.put("allowedTools", allowedTools(intentResult));
    
    String userId = stringParam(chatClientRequest, "userId");
    String contextBlock = buildContextBlock(chatClientRequest, intentResult, userId);
    
    // 2. 使用 Builder 模式重新生成不可变的 ChatClientRequest,防止高并发下 unmodifiableMap 修改报错
    ChatClientRequest.Builder builder = chatClientRequest.mutate().context(context);
    if (StringUtils.hasText(contextBlock)) {
        List<Message> instructions = new ArrayList<>(chatClientRequest.prompt().getInstructions());
        instructions.add(0, new SystemMessage(contextBlock)); // 将用户记忆上下文强插首位以防长上下文失忆
        builder.prompt(new Prompt(instructions, chatClientRequest.prompt().getOptions()));
    }
    return callAdvisorChain.nextCall(builder.build());
}

// 动态意图工具白名单授权(沙箱权限隔离)
private Set<String> allowedTools(IntentRecognitionResult intentResult) {
    if (intentResult == null || !intentResult.intent().isTask()) return Set.of();
    
    // 第一级:domain 基础工具集
    Set<String> base = switch (intentResult.intent().domain()) {
        case ORDER   -> setOf("searchOrders", "getOrderDetail", "listRecentOrders");
        case MENU    -> setOf("searchDishes", "searchSetmeals");
        case ADDRESS -> setOf("searchAddresses", "listAddresses");
        case SHOP    -> setOf("getShopStatus");
    };
    // 第二级:intent 专属操作工具
    Set<String> extra = switch (intentResult.intent()) {
        case CANCEL_ORDER   -> setOf("cancelOrder");
        case REQUEST_REFUND -> setOf("requestRefund");
        case CHANGE_ADDRESS -> setOf("updateDeliveryAddress");
        default             -> Set.of();
    };
    Set<String> merged = new LinkedHashSet<>(base);
    merged.addAll(extra);
    return merged;
}

3. 工具调用安全与【SafeToolCallAdvisor 熔断机制】

  • 面试官切入点

    "在生产环境中,Agent 的工具调用可能出现死循环——比如模型反复调用同一个工具且参数一致。你是如何防止这种情况的?"

  • 袁志刚专属特训回答模版
    1. 工具调用死循环的生产危害:在自回归 Loop 中,如果大模型生成的工具入参有轻微语病,后端 API 返回错误码,大模型往往会进入“反思重试”分支,然而由于推理的局限,它会带着完全一致的错误参数,继续调用该工具。这会形成一个永无止境的死循环网络往返,直接耗尽用户的 Token 配额并卡死系统。
    2. 签名哈希防重检测算法:我们在 sky-ai 中自定义编写了 SafeToolCallAdvisor。每次拦截到大模型发起的 tool_calls,我们都会将 toolCall.name() 与格式化清洗后的 toolCall.arguments() 拼接,通过 MD5 或字符串哈希生成唯一的 “调用签名”。利用 LoopState 内部类,跨轮次地将已发生的签名记录在 seenToolCalls Set 集合中。
    3. 两层强熔断设计与优雅话术兜底
      • 跨轮签名重复:一旦在当前会话的 Loop 中,检测到新发起的工具调用签名在 seenToolCalls 中已经存在,立即判定发生死循环,强行熔断拦截。
      • 轮次极限硬熔断:单次用户请求的工具交互轮次累加至 4 轮时,若大模型仍未输出最终回复,判定其规划受挫,触发极限硬熔断。
      • 静默兜底降级:熔断触发后,拦截器不会无脑地抛出异常,而是构建一个包含 STOP_MESSAGEGeneration 降级对象覆写 ChatResponse,输出给用户:“已为您查询到的信息不足以自动继续处理,请确认您的具体菜品,或换一种明确的说法。”,既阻止了 Token 的无谓空转,又保障了用户交互的连贯体验。

🖥️ 核心支撑源码:工具调用签名防重与死循环熔断

java
// SafeToolCallAdvisor.java - 死循环防重熔断拦截器
private ChatClientResponse guard(ChatClientResponse chatClientResponse) {
    if (chatClientResponse == null || chatClientResponse.chatResponse() == null
            || chatClientResponse.chatResponse().getResult() == null) {
        return chatClientResponse;
    }
    AssistantMessage assistantMessage = chatClientResponse.chatResponse().getResult().getOutput();
    if (assistantMessage == null || !assistantMessage.hasToolCalls()) {
        return chatClientResponse;
    }
    
    LoopState loopState = state(chatClientResponse.context());
    Set<String> signatures = new HashSet<>();
    for (AssistantMessage.ToolCall toolCall : assistantMessage.getToolCalls()) {
        String signature = toolCall.name() + "\u0000" + (toolCall.arguments() == null ? "" : toolCall.arguments().trim());
        // 核心拦截:检测当前轮次是否重复,或者是否调用了历史已见过的相同参数工具
        if (!signatures.add(signature) || loopState.seenToolCalls.contains(signature)) {
            return stop(chatClientResponse); // 熔断拦截,短路大模型并输出兜底话术
        }
    }
    if (loopState.toolCallRounds >= MAX_TOOL_CALL_ROUNDS) { // 轮次超限强熔断(防止无限 Loop 耗尽 Token)
        return stop(chatClientResponse);
    }
    loopState.toolCallRounds++;
    loopState.seenToolCalls.addAll(signatures);
    return chatClientResponse;
}

private ChatClientResponse stop(ChatClientResponse chatClientResponse) {
    ChatResponse fallback = ChatResponse.builder()
            .from(chatClientResponse.chatResponse())
            .generations(List.of(new Generation(AssistantMessage.builder().content(STOP_MESSAGE).build())))
            .build();
    return chatClientResponse.mutate().chatResponse(fallback).build();
}

🖥️ 核心支撑源码:工具水平越权与参数穿透过滤(OrderTools 拦截防护)

java
// OrderTools.java - 工具调用水平越权防御
@Tool(description = "Cancel an unpaid or unconfirmed order for the current user.")
public String cancelOrder(@ToolParam(description = "Order number or internal order id") String orderRef, ToolContext context) {
    try {
        // 1. 通过缓存匹配解析 OrderId,防御模型幻觉瞎编的参数穿透
        Long orderId = resolveOrderId(orderRef, context);
        // 2. 强绑定当前经过鉴权的当前用户 ID 传递给网关,底层做水平越权(ID一致性)校验
        return orderGateway.cancelOrder(ToolUser.userId(context), orderId.toString());
    } catch (IllegalArgumentException ex) {
        return ex.getMessage();
    }
}

private Long resolveOrderId(String orderRef, ToolContext context) {
    String trimmedOrderRef = orderRef == null ? "" : orderRef.trim();
    // 强制限制解析必须比对最近缓存订单列表,非法或无权限的订单 id 无法通过该校验,杜绝了参数盲猜和水平越权漏洞
    List<JsonNode> recentOrders = recentOrders(context);
    for (JsonNode order : recentOrders) {
        if (trimmedOrderRef.equals(text(order, "number")) || trimmedOrderRef.equals(longValue(order, "id").toString())) {
            return longValue(order, "id");
        }
    }
    throw new IllegalArgumentException("未找到对应订单,请提供正确的订单号或先查询最近订单。");
}